package org.seefin.formats.iso8583.types;

import java.util.Arrays;
import java.util.BitSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;



/**
 * Implementation of the ISO8583 bitmap type, with facilities to create, parse and format a
 * message's bitmap in a number of formats
 * 
 * @author phillipsr
 *
 */
public class Bitmap
{
	private static final Matcher HexMatcher = Pattern.compile ( "[0-9A-Fa-f]+").matcher("");
	public enum Id
	{
		PRIMARY(0), SECONDARY(1), TERTIARY(2);
		private final int index;
		private Id(int index) { this.index = index; }
	}
	/** bitsets hold the three bitmaps available to a message, and
	   are indexed by the <code>Id.index</code> from the enum above */
	private BitSet[] bitmaps 
		= new BitSet[] { new BitSet(), new BitSet(), new BitSet()};
	
	public Bitmap() {}
	
	/**
	 * Create a bitmap, instantiating it from the supplied Hex string, which 
	 * may represent a primary, primary + secondary or primary + secondary + tertiary
	 * bitmap
	 * 
	 * @param hexBitmap hexadecimal representation of a bitmap
	 * @throws IllegalArgumentException if the bitmap is not a 16, 32 or 48 byte long
	 * 					hexadecimal string
	 */
	public static Bitmap
	parse ( String hexBitmap)
	{
		if ( hexBitmap == null)
		{
			throw new IllegalArgumentException ( "Hex bitmap must not be null");
		}
		hexBitmap = hexBitmap.trim();
		int hexlength = hexBitmap.length ();
		if ( hexlength != 16 && hexlength != 32 && hexlength != 48)
		{
			throw new IllegalArgumentException ( 
					"Hex bitmap must be 16, 32 or 48 characters in size (got: " + hexlength + " chars)");
		}
		if ( HexMatcher.reset ( hexBitmap).matches () == false)
		{
			throw new IllegalArgumentException ( "Hex bitmap must contain only hexadecimal digits (0-9A-F)");
		}
		Bitmap result = new Bitmap();
		result.bitmaps[Id.PRIMARY.index] = BitsetUtil.hex2Bitset ( hexBitmap.substring(0,16));
		if ( hexlength > 16)
		{
			result.bitmaps[Id.SECONDARY.index] = BitsetUtil.hex2Bitset ( hexBitmap.substring(16,32));
		}
		if ( hexlength > 32)
		{
			result.bitmaps[Id.TERTIARY.index] = BitsetUtil.hex2Bitset ( hexBitmap.substring(32));
		}
		return result;
	}

	/**
	 * @param binBitmap1
	 */
	public Bitmap ( byte[] binBitmap)
	{
		if ( binBitmap == null || binBitmap.length < 8)
		{
			throw new IllegalArgumentException ( "Bin bitmap must be >= 8 bytes in size");
		}
		int length = binBitmap.length;
		if ( length > 16)
		{
			bitmaps[Id.TERTIARY.index] = BitsetUtil.bin2Bitset ( Arrays.copyOfRange ( binBitmap, 16, 24));
		}
		if ( length > 8)
		{
			bitmaps[Id.SECONDARY.index] = BitsetUtil.bin2Bitset ( Arrays.copyOfRange ( binBitmap, 8, 16));
		}
		bitmaps[Id.PRIMARY.index] = BitsetUtil.bin2Bitset ( Arrays.copyOfRange ( binBitmap, 0, 9));
	}

	public String
	asHex ( Id map)
	{
		return BitsetUtil.bitset2Hex ( bitmaps[map.index], 16);
	}

	public byte[]
	asBinary ( Id map)
	{
		return BitsetUtil.bitset2bin ( bitmaps[map.index], 8);		
	}

	public void clear ()
	{
		bitmaps[Id.PRIMARY.index].clear ();
		bitmaps[Id.SECONDARY.index].clear ();
		bitmaps[Id.TERTIARY.index].clear ();
	}

	/**
	 * @param fieldNb
	 * @throws IllegalArgumentException if the fieldNb is not {2..64} or {66..128} or {130..192}
	 */
	public void
	setField ( int fieldNb)
	{
		if ( fieldNb < 2 || fieldNb > 192 || fieldNb == 65 || fieldNb == 129)
		{
			throw new IllegalArgumentException ( 
					"fieldNb can only be: {2..64} or {66..128} or {130..192} (fieldNb=" + fieldNb + ")");
		}
		Id bitmapId = getBitmapIdForField ( fieldNb);
		BitSet bitmap = bitmaps[bitmapId.index];
		bitmap.set ( getFieldPosInBitmap ( fieldNb, bitmapId));
		setBitmapPresent ( bitmapId);
	}
	
	/**
	 * @param fieldNb
	 * @return
	 */
	private Id
	getBitmapIdForField ( int fieldNb)
	{
		if ( fieldNb <= 64)
		{
			return Id.PRIMARY;
		}
		if ( fieldNb > 64 && fieldNb <= 128)
		{
			return Id.SECONDARY;
		}
		return Id.TERTIARY;
	}
	
	private int
	getFieldPosInBitmap ( int fieldNb, Id bitmapId)
	{
		switch ( bitmapId)
		{
			case PRIMARY:
				return fieldNb-1;
			case SECONDARY:
				return fieldNb-65;
			case TERTIARY:
				return fieldNb-129;
			default:
				return 0; // unreachable as Id is enum
		}
	}

	/**
	 * @param fieldNb 
	 * @return
	 */
	public boolean
	isFieldPresent ( int fieldNb)
	{
		Id bitmapId = getBitmapIdForField ( fieldNb);
		BitSet bitmap = bitmaps[bitmapId.index];
		int pos = getFieldPosInBitmap ( fieldNb, bitmapId);
		return bitmap.get ( pos);
	}
	
	public boolean
	isBitmapPresent(Id map)
	{
		if ( map == Id.PRIMARY)
		{
			return true;
		}
		BitSet bitmap = bitmaps[map.index-1];
		return bitmap.get ( 0);
	}
	
	private void
	setBitmapPresent ( Id bitmapId)
	{
		if ( bitmapId == Id.PRIMARY)
		{
			return; // always present
		}
		bitmaps[bitmapId.index-1].set ( 0);
		
		// if tertiary bitmap is present, then secondary is implicitly present
		if ( bitmapId == Id.TERTIARY)
		{
			setBitmapPresent ( Id.SECONDARY);
		}
	}
	
	@Override
	public String
	toString()
	{
		StringBuilder result = new StringBuilder();
		result.append ( BitsetUtil.bitset2Hex ( bitmaps[Id.PRIMARY.index], 16));
		if ( isBitmapPresent ( Id.SECONDARY) == true)
		{
			result.append ( BitsetUtil.bitset2Hex ( bitmaps[Id.SECONDARY.index], 16));
			if ( isBitmapPresent ( Id.TERTIARY) == true)
			{
				result.append ( BitsetUtil.bitset2Hex ( bitmaps[Id.TERTIARY.index], 16));
			}
		}
		return result.toString ();
	}
	
}
